package com.example.authorization_server.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

import javax.annotation.Resource;

import static com.example.common.constant.Constant.*;
import static com.example.common.constant.Constant.ClientScope.ADD;
import static com.example.common.constant.Constant.ResourceServerId.ORDER_SERVICE;

/**
 * @author 罗俊华
 * @date 2022/3/10 - 2:57 下午
 */
@Slf4j
@Configuration
// 标记此服务为一个授权服务
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private TokenStore tokenStore;

    @Resource
    private ClientDetailsService clientDetailsService;

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private PasswordEncoder passwordEncoder ;


    /**
     * 配置授权码模式下的授权码如何存储
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {

        return new InMemoryAuthorizationCodeServices();
    }


    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {

        DefaultTokenServices result = new DefaultTokenServices();

        /**
         * 提供 OAuth 2.0 中 客户端（client）的id和secret
         */
        result.setClientDetailsService(clientDetailsService);

//        是否支持刷新令牌
        result.setSupportRefreshToken(true);

//        配置令牌的存储策略
        result.setTokenStore(tokenStore);

//        令牌的存活时间
        result.setAccessTokenValiditySeconds(600);

//          刷新令牌的存活时间
        result.setRefreshTokenValiditySeconds(10);

        return result;
    }

    /**
     * 配置认证用户的信息来源(ClientDetailsService)
     * 客户端的详情信息在这里初始化
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {


/**一、authorization_code 授权码模式：
 * 一般用于服务器之间的相互授权，最安全
 * 这里配置的用户信息，当client请求授权码的时候，请求时的信息必须和这里填入的用户信息一致，否则OAuth异常
 * http://127.0.0.1:8801/oauth/authorize?client_id=luo&response_type=code&scope=all&redirect_uri=https://www.baidu.com
 * 用户输入了 UserDetailService 中配置的用户，并点击同意（approve）按钮之后，将跳转 redirect_uri 地址
 * https://www.baidu.com/?code=TIKC0z
 * 跳转之后的地址就包含了 授权码 code=TIKC0z
 * 然后，第三方拿着这个授权码并带上自己的 client_id client_secret 一并提交给授权服务器（authorization server），将其兑换为 token
 * http://localhost:8801/oauth/token?client_id=luo&client_secret=1234&code=TIKC0z&grant_type=authorization_code&redirect_uri=https://www.baidu.com
 * 访问了之后，得到的token为
 * {"access_token":"0d9cf9af-3b64-4e6e-97fc-ed1aaf4e8e8e","token_type":"bearer","refresh_token":"792f61e1-ddef-4ec1-b7ed-d73c13b5ad90","expires_in":9,"scope":"all"}
 *
 * 二、简化模式
 * 此模式一般用于没有服务端的第三方单页面应用，返回的uri中以 # 号作为分隔
 * 登录成功之后直接获得 access_token
 * 和第一种模式的区别是：请求时的 response_type=token
 * http://127.0.0.1:8801/oauth/authorize?client_id=luo&response_type=token&scope=all&redirect_uri=https://www.baidu.com
 * 同意（approve）之后，会重定向，重定向返回的是 access_token，而不是 授权码 grant_code
 * https://www.baidu.com/#access_token=b95db13c-e8aa-455e-be6a-958398bc6626&token_type=bearer&expires_in=10
 *
 * 三、密码模式
 * 此模式十分简单，但是会把密码泄露给客户端
 * 一般用于 client 和 authorization server 是同一个公司开发的情况，第一方原生应用、第一方单页面应用
 * 1、资源拥有者将 用户名、密码发送给客户端
 * 2、客户端在此基础之上再把自己的 client_id client_secret 发送给 authorization server 请求令牌
 * http://localhost:8801/oauth/token?client_id=luo&client_secret=1234&grant_type=password&username=我是用户&password=1234
 * 返回的响应
 * {"access_token":"c9a60f99-fa96-4e25-8df3-ecdb7d7cf079","token_type":"bearer","refresh_token":"0da6e515-1340-4037-ac0d-9d4eac52328c","expires_in":10,"scope":"all"}
 *
 * 四、客户端模式
 * 最不安全，要求我们对client完全信任，而且client本身也是安全的
 * 因此，此模式一般用来提供给我们完全信任的服务器
 * http://localhost:8801/oauth/token?client_id=luo&client_secret=1234&grant_type=client_credentials
 * 响应
 * {"access_token":"55b7e49b-da36-4a8e-988d-8c73833e3c6f","token_type":"bearer","expires_in":10,"scope":"all"}
 */
//      配置内存中的用户
        clients.inMemory()
//                clientId 用于第三方的唯一标识
                .withClient("luo")
                //需要值得信任的客户端，的密钥
                .secret(passwordEncoder.encode("1234"))
                // 可以访问的资源服务器列表，每一个资源服务器都有一个资源id
                .resourceIds(ORDER_SERVICE)
                // 此client 允许的授权类型
                .authorizedGrantTypes("password", "authorization_code", "client_credentials", "implicit", "refresh_token")
                // 用于限制此client的访问范围，可以暂时理解为客户端的权限
                .scopes(ADD)
                // false 为跳转到授权页面
                // true 不跳转授权页面，直接发放令牌
                .autoApprove(false)
                // 回调的地址
                .redirectUris("https://www.baidu.com")

                .and()
                .withClient("yan")
                .secret(passwordEncoder.encode("1234"))
                .authorities(ROLE_USER)


                .and()
                .withClient("wang")
                .secret(passwordEncoder.encode("1234"))
                .authorities(ROLE_ADMIN)

                .and()
                .withClient(ResourceServiceAccount.CLIENT_ID)
                .secret(passwordEncoder.encode(ResourceServiceAccount.CLIENT_SECRET))
        ;
    }

    /**
     * 什么条件下允许 请求访问 OAuth2.0 的令牌端点
     * 令牌访问端点安全策略
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {


//        公开 /oauth/token_key
        security.tokenKeyAccess("permitAll()")
//                公开 /oauth/check_token ，用于其他资源服务校验收到的token的合法性
                .checkTokenAccess("permitAll()")
                // 允许通过表单认证来申请令牌
                .allowFormAuthenticationForClients()


        ;

    }


    /**
     * 配置申请令牌需要访问的端点的安全策略，以及令牌管理相关的服务
     * 配置授权访问相关的端点
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                // OAuth 2 密码授权类型时所必须
                .authenticationManager(authenticationManager)
//                .userDetailsService(userDetailsService)
//                OAuth 2 授权码模式(authorization_code)所需
                .authorizationCodeServices(authorizationCodeServices())
//                配置令牌管理服务（必须）
                .tokenServices(authorizationServerTokenServices())
//                只允许 post get 提交
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);


    }
}
